package com.demo.redis.config;

import com.demo.redis.springbootcache.annotation.CacheExpire;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.*;

/**
 * 自定义RedisCacheManger，实现缓存过期功能和缓存在redis的对象转成json格式
 * @author cyj
 */
@Slf4j
public class MyRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean {

    private ApplicationContext applicationContext;

    private final Map<String, RedisCacheConfiguration> initialCacheConfiguration = new LinkedHashMap<>();

    public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 初始化每个key的配置
     */
    @Override
    public void afterPropertiesSet() {
        String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
        for (String beanName : beanNames) {
            final Class<?> clazz = applicationContext.getType(beanName);
            add(clazz);
        }
        super.afterPropertiesSet();
    }

    /**
     * 转换缓存配置集合，通过父类{@link #initializeCaches()}方法加载配置，然后封装成cacheMap对象。
     * 调用{@link #getCache(String name)}方法时，没有在cacheMap对象找到对应缓存key配置，
     * 就通过{@link #createRedisCache(String name, RedisCacheConfiguration cacheConfig)}读取默认配置并放在cacheMap对象
     * @return Collection
     */
    @Override
    @NonNull
    protected Collection<RedisCache> loadCaches() {
        List<RedisCache> caches = new LinkedList<>();
        for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
            caches.add(super.createRedisCache(entry.getKey(), entry.getValue()));
        }
        return caches;
    }

    /**
     * 设置每一个key的时效时间
     * @param clazz class对象
     */
    private void add(final Class<?> clazz) {
        ReflectionUtils.doWithMethods(clazz, method -> {
            ReflectionUtils.makeAccessible(method);
            CacheExpire cacheExpire = AnnotationUtils.findAnnotation(method, CacheExpire.class);
            if (cacheExpire == null) {
                return;
            }
            Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);
            if (cacheable != null) {
                log.info("使用Springboot cache对象和方法，class：{}, method:{}", clazz, method);
                final JavaType javaType = getReturnJavaType(method);
                add(cacheable.cacheNames(), javaType, cacheExpire);
                return;
            }
            Caching caching = AnnotationUtils.findAnnotation(method, Caching.class);
            if (caching != null) {
                Cacheable[] cs = caching.cacheable();
                if (cs.length > 0) {
                    for (Cacheable c : cs) {
                        if (c != null) {
                            final JavaType javaType = getReturnJavaType(method);
                            add(c.cacheNames(), javaType, cacheExpire);
                        }
                    }
                }
            } else {
                CacheConfig cacheConfig = AnnotationUtils.findAnnotation(clazz, CacheConfig.class);
                if (cacheConfig != null) {
                    final JavaType javaType = getReturnJavaType(method);
                    add(cacheConfig.cacheNames(), javaType, cacheExpire);
                }
            }
        }, method -> null != AnnotationUtils.findAnnotation(method, CacheExpire.class));
    }

    private void add(String[] cacheNames, @NonNull JavaType javaType, CacheExpire cacheExpire) {
        for (String cacheName : cacheNames) {
            if (cacheName == null || "".equals(cacheName.trim())) {
                continue;
            }
            long expire = cacheExpire.expire();
            log.info("javaType:{}, cacheName: {}, expire: {}", javaType, cacheName, expire);
            if (expire >= 0) {
                // 缓存配置,设置每个key超时时间
                RedisCacheConfiguration config = CacheManagerConfig.getConfig(javaType).entryTtl(Duration.ofSeconds(expire));
                initialCacheConfiguration.put(cacheName, config);
            } else {
                log.warn("{} use default expiration.", cacheName);
            }
        }
    }

    private JavaType getReturnJavaType(Method method) {
        // 获取方法返回类型
        final Class<?> returnType = method.getReturnType();
        // 获取泛型类型
        final Type genericReturnType = method.getGenericReturnType();
        //获取返回值的泛型参数
        if (genericReturnType instanceof ParameterizedType) {
            List<Class<?>> list = new ArrayList<>();
            Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
            for (Type type : actualTypeArguments) {
                log.info("方法返回值的泛型 type:{} " , type);
                if (type instanceof Class) {
                    list.add((Class<?>) type);
                }
            }
            Class<?>[] classes = new Class[list.size()];
            return TypeFactory.defaultInstance().constructParametricType(returnType, list.toArray(classes));
        }
        return TypeFactory.defaultInstance().constructType(returnType);
    }
}
